﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Lua;
using System.Lua.Serialization;
using System.Threading;

namespace Microsoft.Coding4Fun.TweetCraft.Wow
{
    /// <summary>
    /// Implements a communication channel with World of Warcraft AddOns through a SavedVariables file.
    /// </summary>
    public sealed class SavedVariablesChannel : ValueChannelBase, ISavedVariablesChannel, IReportsError, IDisposable
    {
        private const int retryInMilliseconds = 10;

        private readonly string savedVariablesPath;

        private FileSystemWatcher savedVariablesWatcher;
        private LuaReader luaReader;

        /// <summary>
        /// Initializes a new instance of the <see cref="SavedVariablesChannel"/> class.
        /// </summary>
        /// <param name="savedVariablesPath">The path to the SavedVariables file.</param>
        public SavedVariablesChannel(string savedVariablesPath)
        {
            if (savedVariablesPath == null)
                throw new ArgumentNullException("savedVariablesPath");

            this.savedVariablesPath = savedVariablesPath;

            this.InitializeReader();
            this.InitializeWatcher();
        }

        private bool InitializeReader()
        {
            try
            {
                // Dispose existing Lua reader, if there is one
                if (luaReader != null)
                {
                    luaReader.Dispose();
                    luaReader = null;
                }

                // If the SavedVariables file exists, create new LuaReader

                // As long as the file exists, try to read content,
                // the file might be still written to or used by World of Warcraft
                while (File.Exists(savedVariablesPath))
                {
                    using (TraceScope trace = TraceScope.Information("Parsing SavedVariables file..."))
                    {
                        try
                        {
                            // Read Lua chunk
                            string chunk = File.ReadAllText(savedVariablesPath);

                            // Create LuaReader with chunk
                            luaReader = LuaReader.Create(chunk);

                            trace.Complete("Parse complete, {0} variables defined.", luaReader.VariableCount);

                            return true;
                        }
                        catch (IOException exception)
                        {
                            // Trace the IO exception message
                            Trace.TraceInformation("Could not access SavedVariables file ({0}), retrying...", exception.Message);

                            // The file is still being used, put the thread to sleep and retry
                            Thread.Sleep(retryInMilliseconds);
                        }
                    }
                }
            }
            catch (CouldNotInterpretException exception)
            {
                // Report error, channel is left in a valid state (albeit no channel information is available)
                this.OnError(exception);
            }

            return false;
        }

        private void InitializeWatcher()
        {
            // Get the directory from the path to the SavedVariables file
            string savedVariablesDirectory = Path.GetDirectoryName(savedVariablesPath);

            // Make sure that the folder that we're about to watch, exists
            if (!Directory.Exists(savedVariablesDirectory))
                Directory.CreateDirectory(savedVariablesDirectory);

            // Initialize filesystem watcher
            savedVariablesWatcher = new FileSystemWatcher(savedVariablesDirectory, Path.GetFileName(savedVariablesPath));

            // Subscribe to change events
            savedVariablesWatcher.Created += OnSavedVariablesChanged;
            savedVariablesWatcher.Changed += OnSavedVariablesChanged;

            // Enable the watcher
            savedVariablesWatcher.EnableRaisingEvents = true;

            Trace.TraceInformation("Watching SavedVariables file for changes...");
        }

        /// <summary>
        /// Gets the path to the SavedVariables file.
        /// </summary>
        public string SavedVariablesPath
        {
            get { return savedVariablesPath; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to auto flush queued values after a
        /// <see cref="IValueChannel.ChannelUpdated"/> event.
        /// </summary>
        public bool AutoFlush { get; set; }

        /// <summary>
        /// Gets a value indicating whether a value with the given key is available in the channel.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>
        /// 	<c>true</c>, if the value is available; <c>false</c> otherwise.
        /// </returns>
        public override bool IsAvailable(string key)
        {
            if (key == null)
                throw new ArgumentNullException("key");

            if (luaReader == null)
                return false;

            return luaReader.IsVariableDefined(key);
        }

        /// <summary>
        /// Reads a value out of the channel.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="key">The key.</param>
        /// <returns>
        /// An instance of the <typeparamref name="T"/> type, if a value with the key was found; <c>default(<typeparamref name="T"/>)</c> otherwise.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1004", Justification = "The syntax 'var value = valueChannel.ReadValue<int>(...)' is considered better than 'int value = (int)valueChannel.ReadValue(...)'.")]
        public override T ReadValue<T>(string key)
        {
            if (key == null)
                throw new ArgumentNullException("key");

            if (luaReader == null)
                return default(T);

            return LuaSerializer.Deserialize<T>(luaReader, key);
        }

        /// <summary>
        /// Flushes the specified queued values.
        /// </summary>
        /// <param name="queuedValues">The queued values.</param>
        protected override void Flush(IEnumerable<KeyValuePair<string, object>> queuedValues)
        {
            // Disable watcher while performing flush
            savedVariablesWatcher.EnableRaisingEvents = false;

            // Create a new SavedVariables file and initialize a LuaWriter for use with serializing the queued values
            using (StreamWriter streamWriter = new StreamWriter(savedVariablesPath))
            using (LuaWriter luaWriter = LuaWriter.Create(streamWriter))
            using (TraceScope trace = TraceScope.Information("Serializing key / value pairs into SavedVariables file...", Path.GetFileName(savedVariablesPath)))
            {
                // Iterate through the queued values and serialize them as Lua variables
                foreach (KeyValuePair<string, object> pair in queuedValues)
                    LuaSerializer.Serialize(luaWriter, pair.Key, pair.Value);

                trace.Complete("Serialization complete.");
            }

            // Initialize new LuaReader with flushed values
            this.InitializeReader();

            // Re-enable watcher
            savedVariablesWatcher.EnableRaisingEvents = true;
        }

        private void OnSavedVariablesChanged(object sender, FileSystemEventArgs e)
        {
            using (TraceScope trace = TraceScope.Information("SavedVariables file changed..."))
            {
                // Initialize LuaReader with the new SavedVariables file, make sure reader was initialized
                if (this.InitializeReader())
                {
                    // SavedVariables file changed, raise ChannelUpdated event
                    this.OnChannelUpdated();
                }

                // Check whether AutoFlush is enabled
                if (this.AutoFlush)
                {
                    // Flush queued values to saved variables
                    this.Flush();
                }

                trace.Complete("SavedVariables file processed.");
            }
        }

        /// <summary>
        /// Raised when an error occurs.
        /// </summary>
        public event EventHandler<ErrorEventArgs> Error;

        private void OnError(Exception exception)
        {
            if (Error != null)
                Error(this, new ErrorEventArgs(exception));
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (luaReader != null)
            {
                luaReader.Dispose();
                luaReader = null;
            }

            if (savedVariablesWatcher != null)
            {
                savedVariablesWatcher.Dispose();
                savedVariablesWatcher = null;
            }

            GC.SuppressFinalize(this);
        }
    }
}